#!/usr/bin/env python3
#
# Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
"""Generates CSSStyleDeclaration template file from css property definitions
defined in WebKit."""

import tempfile, os, re

COMMENT_LINE_PREFIX = '   * '
# TODO(efortuna): Pull from DEPS so that we have latest css *in sync* with our
# Dartium. Then remove the checked in CSSPropertyNames.in.
SOURCE_PATH = 'CSSPropertyNames.in'
#SOURCE_PATH = 'Source/WebCore/css/CSSPropertyNames.in'
TEMPLATE_FILE = '../templates/html/impl/impl_CSSStyleDeclaration.darttemplate'

# These are the properties that are supported on all Dart project supported
# browsers as camelCased names on the CssStyleDeclaration.
# Note that we do not use the MDN for compatibility info here.
BROWSER_PATHS = [
    'cssProperties.CSS21.txt',  # Remove when we have samples from all browsers.
    'cssProperties.ie9.txt',
    'cssProperties.ie10.txt',
    'cssProperties.ie11.txt',
    'cssProperties.ff36.txt',
    'cssProperties.chrome40.txt',
    'cssProperties.safari-7.1.3.txt',
    'cssProperties.mobileSafari-8.2.txt',
    'cssProperties.iPad4Air.onGoogleSites.txt',
]

# Supported annotations for any specific CSS properties.
annotated = {
    'transition':
    '''@SupportedBrowser(SupportedBrowser.CHROME)
  @SupportedBrowser(SupportedBrowser.FIREFOX)
  @SupportedBrowser(SupportedBrowser.IE, '10')
  @SupportedBrowser(SupportedBrowser.SAFARI)'''
}


class Error:

    def __init__(self, message):
        self.message = message

    def __repr__(self):
        return self.message


def camelCaseName(name):
    """Convert a CSS property name to a lowerCamelCase name."""
    name = name.replace('-webkit-', '')
    words = []
    for word in name.split('-'):
        if words:
            words.append(word.title())
        else:
            words.append(word)
    return ''.join(words)


def dashifyName(camelName):

    def fix(match):
        return '-' + match.group(0).lower()

    return re.sub(r'[A-Z]', fix, camelName)


def isCommentLine(line):
    return line.strip() == '' or line.startswith('#') or line.startswith('//')


def readCssProperties(filename):
    data = open(filename).readlines()
    data = sorted([d.strip() for d in set(data) if not isCommentLine(d)])
    return data


def GenerateCssTemplateFile():
    data = open(SOURCE_PATH).readlines()

    # filter CSSPropertyNames.in to only the properties
    # TODO(efortuna): do we also want CSSPropertyNames.in?
    data = [d.strip() for d in data if not isCommentLine(d) and not '=' in d]

    browser_props = [set(readCssProperties(file)) for file in BROWSER_PATHS]
    universal_properties = set.intersection(*browser_props)
    universal_properties = universal_properties.difference(['cssText'])
    universal_properties = universal_properties.intersection(
        list(map(camelCaseName, data)))

    class_file = open(TEMPLATE_FILE, 'w')

    class_file.write("""
// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// WARNING: DO NOT EDIT THIS TEMPLATE FILE.
// The template file was generated by scripts/css_code_generator.py

// Source of CSS properties:
//   %s

part of $LIBRARYNAME;
""" % SOURCE_PATH)

    class_file.write("""
$(ANNOTATIONS)$(NATIVESPEC)$(CLASS_MODIFIERS)class $CLASSNAME $EXTENDS with
    $(CLASSNAME)Base $IMPLEMENTS {
  factory $CLASSNAME() => new CssStyleDeclaration.css('');

  factory $CLASSNAME.css(String css) {
    final style = new DivElement().style;
    style.cssText = css;
    return style;
  }

  /// Returns the value of the property if the provided *CSS* property
  /// name is supported on this element and if the value is set. Otherwise
  /// returns an empty string.
  ///
  /// Please note the property name uses camelCase, not-hyphens.
  String getPropertyValue(String propertyName) {
    return _getPropertyValueHelper(propertyName);
  }

  String _getPropertyValueHelper(String propertyName) {
    return _getPropertyValue(_browserPropertyName(propertyName));
  }

  /**
   * Returns true if the provided *CSS* property name is supported on this
   * element.
   *
   * Please note the property name camelCase, not-hyphens. This
   * method returns true if the property is accessible via an unprefixed _or_
   * prefixed property.
   */
  bool supportsProperty(String propertyName) {
    return _supportsProperty(propertyName) ||
        _supportsProperty(_camelCase("${Device.cssPrefix}$propertyName"));
  }

  bool _supportsProperty(String propertyName) {
    return JS('bool', '# in #', propertyName, this);
  }


  void setProperty(String propertyName, String$NULLABLE value,
      [String$NULLABLE priority]) {
    return _setPropertyHelper(_browserPropertyName(propertyName),
      value, priority);
  }

  String _browserPropertyName(String propertyName) {
    String$NULLABLE name = _readCache(propertyName);
    if (name is String) return name;
    name = _supportedBrowserPropertyName(propertyName);
    _writeCache(propertyName, name);
    return name;
  }

  String _supportedBrowserPropertyName(String propertyName) {
    if (_supportsProperty(_camelCase(propertyName))) {
      return propertyName;
    }
    var prefixed = "${Device.cssPrefix}$propertyName";
    if (_supportsProperty(prefixed)) {
      return prefixed;
    }
    // May be a CSS variable, just use it as provided.
    return propertyName;
  }

  static final _propertyCache = JS('', '{}');
  static String$NULLABLE _readCache(String key) =>
    JS('String|Null', '#[#]', _propertyCache, key);
  static void _writeCache(String key, String value) {
    JS('void', '#[#] = #', _propertyCache, key, value);
  }

  static String _camelCase(String hyphenated) {
    var replacedMs = JS('String', r'#.replace(/^-ms-/, "ms-")', hyphenated);
    return JS(
        'String',
        r'#.replace(/-([\da-z])/ig,'
            r'function(_, letter) { return letter.toUpperCase();})',
        replacedMs);
  }

  void _setPropertyHelper(String propertyName, String$NULLABLE value,
                          [String$NULLABLE priority]) {
    if (value == null) value = '';
    if (priority == null) priority = '';
    JS('void', '#.setProperty(#, #, #)', this, propertyName, value, priority);
  }

  /**
   * Checks to see if CSS Transitions are supported.
   */
  static bool get supportsTransitions {
    return document.body$NULLASSERT.style.supportsProperty('transition');
  }
$!MEMBERS
""")

    for camelName in sorted(universal_properties):
        property = dashifyName(camelName)
        class_file.write("""
  /** Gets the value of "%s" */
  String get %s => this._%s;

  /** Sets the value of "%s" */
  set %s(String$NULLABLE value) {
    _%s = value == null ? '' : value;
  }
  @Returns('String')
  @JSName('%s')
  String get _%s native;

  @JSName('%s')
  set _%s(String value) native;
    """ % (property, camelName, camelName, property, camelName, camelName,
           camelName, camelName, camelName, camelName))

    class_file.write("""
}

class _CssStyleDeclarationSet extends Object with CssStyleDeclarationBase {
  final Iterable<Element> _elementIterable;
  Iterable<CssStyleDeclaration> _elementCssStyleDeclarationSetIterable;

  _CssStyleDeclarationSet(this._elementIterable)
    : _elementCssStyleDeclarationSetIterable = 
            new List.of(_elementIterable).map((e) => e.style);

  String getPropertyValue(String propertyName) =>
      _elementCssStyleDeclarationSetIterable.first.getPropertyValue(
          propertyName);

  void setProperty(String propertyName, String$NULLABLE value,
      [String$NULLABLE priority]) {
    _elementCssStyleDeclarationSetIterable.forEach((e) =>
        e.setProperty(propertyName, value, priority));
  }

""")

    class_file.write("""
  void _setAll(String propertyName, String$NULLABLE value) {
    value = value == null ? '' : value;
    for (Element element in _elementIterable) {
      JS('void', '#.style[#] = #', element, propertyName, value);
    }
  }
""")

    for camelName in sorted(universal_properties):
        property = dashifyName(camelName)
        class_file.write("""
  /** Sets the value of "%s" */
  set %s(String value) {
    _setAll('%s', value);
  }
    """ % (property, camelName, camelName))

    class_file.write("""

  // Important note: CssStyleDeclarationSet does NOT implement every method
  // available in CssStyleDeclaration. Some of the methods don't make so much
  // sense in terms of having a reasonable value to return when you're
  // considering a list of Elements. You will need to manually add any of the
  // items in the MEMBERS set if you want that functionality.
}

abstract mixin class CssStyleDeclarationBase {
  String getPropertyValue(String propertyName);
  void setProperty(String propertyName, String$NULLABLE value,
      [String$NULLABLE priority]);
""")

    class_lines = []

    seen = set()
    for prop in sorted(data, key=camelCaseName):
        camel_case_name = camelCaseName(prop)
        upper_camel_case_name = camel_case_name[0].upper() + camel_case_name[1:]
        css_name = prop.replace('-webkit-', '')
        base_css_name = prop.replace('-webkit-', '')

        if base_css_name in seen or base_css_name.startswith('-internal'):
            continue
        seen.add(base_css_name)

        comment = '  /** %s the value of "' + base_css_name + '" */'
        class_lines.append('\n')
        class_lines.append(comment % 'Gets')
        if base_css_name in annotated:
            class_lines.append(annotated[base_css_name])
        class_lines.append("""
  String get %s =>
    getPropertyValue('%s');

""" % (camel_case_name, css_name))

        class_lines.append(comment % 'Sets')
        if base_css_name in annotated:
            class_lines.append(annotated[base_css_name])
        class_lines.append("""
  set %s(String value) {
    setProperty('%s', value, '');
  }
""" % (camel_case_name, css_name))

    class_file.write(''.join(class_lines))
    class_file.write('}\n')
    class_file.close()
